diff --git a/assets/src/bundles/save/index.js b/assets/src/bundles/save/index.js
index 49d9ddc2..f167d12a 100644
--- a/assets/src/bundles/save/index.js
+++ b/assets/src/bundles/save/index.js
@@ -1,552 +1,549 @@
/**
* Copyright (C) 2018-2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
-import {csrfPost, handleFetchError, isGitRepoUrl, htmlAlert, removeUrlFragment,
+import {csrfPost, handleFetchError, isGitRepoUrl, htmlAlert,
getCanonicalOriginURL, getHumanReadableDate} from 'utils/functions';
import {swhSpinnerSrc} from 'utils/constants';
import artifactFormRowTemplate from './artifact-form-row.ejs';
import userRequestsFilterCheckboxFn from 'utils/requests-filter-checkbox.ejs';
let saveRequestsTable;
async function originSaveRequest(
originType, originUrl, extraData,
acceptedCallback, pendingCallback, errorCallback
) {
// Actually trigger the origin save request
const addSaveOriginRequestUrl = Urls.api_1_save_origin(originType, originUrl);
$('.swh-processing-save-request').css('display', 'block');
let headers = {};
let body = null;
if (extraData !== {}) {
body = JSON.stringify(extraData);
headers = {
'Content-Type': 'application/json'
};
};
try {
const response = await csrfPost(addSaveOriginRequestUrl, headers, body);
handleFetchError(response);
const data = await response.json();
$('.swh-processing-save-request').css('display', 'none');
if (data.save_request_status === 'accepted') {
acceptedCallback();
} else {
pendingCallback();
}
} catch (response) {
$('.swh-processing-save-request').css('display', 'none');
const errorData = await response.json();
errorCallback(response.status, errorData);
};
}
function addArtifactVersionAutofillHandler(formId) {
// autofill artifact version input with the filename from
// the artifact url without extensions
$(`#swh-input-artifact-url-${formId}`).on('input', function(event) {
const artifactUrl = $(this).val().trim();
let filename = artifactUrl.split('/').slice(-1)[0];
if (filename !== artifactUrl) {
filename = filename.replace(/tar.*$/, 'tar');
const filenameNoExt = filename.split('.').slice(0, -1).join('.');
const artifactVersion = $(`#swh-input-artifact-version-${formId}`);
if (filenameNoExt !== filename) {
artifactVersion.val(filenameNoExt);
}
}
});
}
export function maybeRequireExtraInputs() {
// Read the actual selected value and depending on the origin type, display some extra
// inputs or hide them. This makes the extra inputs disabled when not displayed.
const originType = $('#swh-input-visit-type').val();
let display = 'none';
let disabled = true;
if (originType === 'archives') {
display = 'flex';
disabled = false;
}
$('.swh-save-origin-archives-form').css('display', display);
if (!disabled) {
// help paragraph must have block display for proper rendering
$('#swh-save-origin-archives-help').css('display', 'block');
}
$('.swh-save-origin-archives-form .form-control').prop('disabled', disabled);
if (originType === 'archives' && $('.swh-save-origin-archives-form').length === 1) {
// insert first artifact row when the archives visit type is selected for the first time
$('.swh-save-origin-archives-form').last().after(
artifactFormRowTemplate({deletableRow: false, formId: 0}));
addArtifactVersionAutofillHandler(0);
}
}
export function addArtifactFormRow() {
const formId = $('.swh-save-origin-artifact-form').length;
$('.swh-save-origin-artifact-form').last().after(
artifactFormRowTemplate({
deletableRow: true,
formId: formId
})
);
addArtifactVersionAutofillHandler(formId);
}
export function deleteArtifactFormRow(event) {
$(event.target).closest('.swh-save-origin-artifact-form').remove();
}
const saveRequestCheckboxId = 'swh-save-requests-user-filter';
const userRequestsFilterCheckbox = userRequestsFilterCheckboxFn({
'inputId': saveRequestCheckboxId,
'checked': false // no filtering by default on that view
});
export function initOriginSave() {
$(document).ready(() => {
$.fn.dataTable.ext.errMode = 'none';
// set git as the default value as before
$('#swh-input-visit-type').val('git');
saveRequestsTable = $('#swh-origin-save-requests')
.on('error.dt', (e, settings, techNote, message) => {
$('#swh-origin-save-request-list-error').text('An error occurred while retrieving the save requests list');
console.log(message);
})
.DataTable({
serverSide: true,
processing: true,
language: {
processing: ` `
},
ajax: {
url: Urls.origin_save_requests_list('all'),
data: (d) => {
if (swh.webapp.isUserLoggedIn() && $(`#${saveRequestCheckboxId}`).prop('checked')) {
d.user_requests_only = '1';
}
}
},
searchDelay: 1000,
// see https://datatables.net/examples/advanced_init/dom_toolbar.html and the comments section
// this option customizes datatables UI components by adding an extra checkbox above the table
// while keeping bootstrap layout
dom: '<"row"<"col-sm-3"l><"col-sm-6 text-left user-requests-filter"><"col-sm-3"f>>' +
'<"row"<"col-sm-12"tr>>' +
'<"row"<"col-sm-5"i><"col-sm-7"p>>',
fnInitComplete: function() {
if (swh.webapp.isUserLoggedIn()) {
$('div.user-requests-filter').html(userRequestsFilterCheckbox);
$(`#${saveRequestCheckboxId}`).on('change', () => {
saveRequestsTable.draw();
});
}
},
columns: [
{
data: 'save_request_date',
name: 'request_date',
render: getHumanReadableDate
},
{
data: 'visit_type',
name: 'visit_type'
},
{
data: 'origin_url',
name: 'origin_url',
render: (data, type, row) => {
if (type === 'display') {
let html = '';
const sanitizedURL = $.fn.dataTable.render.text().display(data);
if (row.save_task_status === 'succeeded') {
if (row.visit_status === 'full' || row.visit_status === 'partial') {
let browseOriginUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(sanitizedURL)}`;
if (row.visit_date) {
browseOriginUrl += `×tamp=${encodeURIComponent(row.visit_date)}`;
}
html += `${sanitizedURL} `;
} else {
const tooltip = 'origin was successfully loaded, waiting for data to be available in database';
html += `${sanitizedURL} `;
}
} else {
html += sanitizedURL;
}
html += ` ` +
' ';
return html;
}
return data;
}
},
{
data: 'save_request_status',
name: 'status'
},
{
data: 'save_task_status',
name: 'loading_task_status'
},
{
name: 'info',
render: (data, type, row) => {
if (row.save_task_status === 'succeeded' || row.save_task_status === 'failed' ||
row.note != null) {
return ` `;
} else {
return '';
}
}
},
{
render: (data, type, row) => {
if (row.save_request_status === 'accepted') {
const saveAgainButton =
'` +
' ' +
'Save again ';
return saveAgainButton;
} else {
return '';
}
}
}
],
scrollY: '50vh',
scrollCollapse: true,
order: [[0, 'desc']],
responsive: {
details: {
type: 'none'
}
}
});
swh.webapp.addJumpToPagePopoverToDataTable(saveRequestsTable);
- $('#swh-origin-save-requests-list-tab').on('shown.bs.tab', () => {
+ if (window.location.pathname === Urls.origin_save() && window.location.hash === '#requests') {
+ // Keep old URLs to the save list working
+ window.location = Urls.origin_save_list();
+ } else if ($('#swh-origin-save-requests')) {
saveRequestsTable.draw();
- window.location.hash = '#requests';
- });
-
- $('#swh-origin-save-request-help-tab').on('shown.bs.tab', () => {
- removeUrlFragment();
- $('.swh-save-request-info').popover('dispose');
- });
+ }
const saveRequestAcceptedAlert = htmlAlert(
'success',
'The "save code now" request has been accepted and will be processed as soon as possible.',
true
);
const saveRequestPendingAlert = htmlAlert(
'warning',
'The "save code now" request has been put in pending state and may be accepted for processing after manual review.',
true
);
const saveRequestRateLimitedAlert = htmlAlert(
'danger',
'The rate limit for "save code now" requests has been reached. Please try again later.',
true
);
const saveRequestUnknownErrorAlert = htmlAlert(
'danger',
'An unexpected error happened when submitting the "save code now request".',
true
);
$('#swh-save-origin-form').submit(async event => {
event.preventDefault();
event.stopPropagation();
$('.alert').alert('close');
if (event.target.checkValidity()) {
$(event.target).removeClass('was-validated');
const originType = $('#swh-input-visit-type').val();
let originUrl = $('#swh-input-origin-url').val();
originUrl = await getCanonicalOriginURL(originUrl);
// read the extra inputs for the 'archives' type
const extraData = {};
if (originType === 'archives') {
extraData['archives_data'] = [];
for (let i = 0; i < $('.swh-save-origin-artifact-form').length; ++i) {
extraData['archives_data'].push({
'artifact_url': $(`#swh-input-artifact-url-${i}`).val(),
'artifact_version': $(`#swh-input-artifact-version-${i}`).val()
});
}
}
originSaveRequest(originType, originUrl, extraData,
() => $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert),
() => $('#swh-origin-save-request-status').html(saveRequestPendingAlert),
(statusCode, errorData) => {
$('#swh-origin-save-request-status').css('color', 'red');
if (statusCode === 403) {
const errorAlert = htmlAlert('danger', `Error: ${errorData['reason']}`);
$('#swh-origin-save-request-status').html(errorAlert);
} else if (statusCode === 429) {
$('#swh-origin-save-request-status').html(saveRequestRateLimitedAlert);
} else if (statusCode === 400) {
const errorAlert = htmlAlert('danger', errorData['reason']);
$('#swh-origin-save-request-status').html(errorAlert);
} else {
$('#swh-origin-save-request-status').html(saveRequestUnknownErrorAlert);
}
});
} else {
$(event.target).addClass('was-validated');
}
});
$('#swh-show-origin-save-requests-list').on('click', (event) => {
event.preventDefault();
$('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
});
$('#swh-input-origin-url').on('input', function(event) {
const originUrl = $(this).val().trim();
$(this).val(originUrl);
$('#swh-input-visit-type option').each(function() {
const val = $(this).val();
if (val && originUrl.includes(val)) {
$(this).prop('selected', true);
// origin URL input need to be validated once new visit type set
validateSaveOriginUrl($('#swh-input-origin-url')[0]);
}
});
});
if (window.location.hash === '#requests') {
$('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
}
$(window).on('hashchange', () => {
if (window.location.hash === '#requests') {
$('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
} else {
$('.nav-tabs a[href="#swh-origin-save-requests-create"]').tab('show');
}
});
});
}
export function validateSaveOriginUrl(input) {
const originType = $('#swh-input-visit-type').val();
let originUrl = null;
let validUrl = true;
try {
originUrl = new URL(input.value.trim());
} catch (TypeError) {
validUrl = false;
}
if (validUrl) {
const allowedProtocols = ['http:', 'https:', 'svn:', 'git:', 'rsync:', 'pserver:', 'ssh:', 'bzr:'];
validUrl = (
allowedProtocols.find(protocol => protocol === originUrl.protocol) !== undefined
);
}
if (validUrl && originType === 'git') {
validUrl = isGitRepoUrl(originUrl);
}
if (validUrl) {
input.setCustomValidity('');
} else {
input.setCustomValidity('The origin url is not valid or does not reference a code repository');
}
}
export function initTakeNewSnapshot() {
const newSnapshotRequestAcceptedAlert = htmlAlert(
'success',
'The "take new snapshot" request has been accepted and will be processed as soon as possible.',
true
);
const newSnapshotRequestPendingAlert = htmlAlert(
'warning',
'The "take new snapshot" request has been put in pending state and may be accepted for processing after manual review.',
true
);
const newSnapshotRequestRateLimitAlert = htmlAlert(
'danger',
'The rate limit for "take new snapshot" requests has been reached. Please try again later.',
true
);
const newSnapshotRequestUnknownErrorAlert = htmlAlert(
'danger',
'An unexpected error happened when submitting the "save code now request".',
true
);
$(document).ready(() => {
$('#swh-take-new-snapshot-form').submit(event => {
event.preventDefault();
event.stopPropagation();
const originType = $('#swh-input-visit-type').val();
const originUrl = $('#swh-input-origin-url').val();
const extraData = {};
originSaveRequest(originType, originUrl, extraData,
() => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestAcceptedAlert),
() => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestPendingAlert),
(statusCode, errorData) => {
$('#swh-take-new-snapshot-request-status').css('color', 'red');
if (statusCode === 403) {
const errorAlert = htmlAlert('danger', `Error: ${errorData['detail']}`, true);
$('#swh-take-new-snapshot-request-status').html(errorAlert);
} else if (statusCode === 429) {
$('#swh-take-new-snapshot-request-status').html(newSnapshotRequestRateLimitAlert);
} else {
$('#swh-take-new-snapshot-request-status').html(newSnapshotRequestUnknownErrorAlert);
}
});
});
});
}
export function formatValuePerType(type, value) {
// Given some typed value, format and return accordingly formatted value
const mapFormatPerTypeFn = {
'json': (v) => JSON.stringify(v, null, 2),
'date': (v) => new Date(v).toLocaleString(),
'raw': (v) => v,
'duration': (v) => v + ' seconds'
};
return value === null ? null : mapFormatPerTypeFn[type](value);
}
export async function displaySaveRequestInfo(event, saveRequestId) {
event.stopPropagation();
const saveRequestTaskInfoUrl = Urls.origin_save_task_info(saveRequestId);
// close popover when clicking again on the info icon
if ($(event.target).data('bs.popover')) {
$(event.target).popover('dispose');
return;
}
$('.swh-save-request-info').popover('dispose');
$(event.target).popover({
animation: false,
boundary: 'viewport',
container: 'body',
title: 'Save request task information ' +
' `,
content: `
Fetching task information ...
`,
html: true,
placement: 'left',
sanitizeFn: swh.webapp.filterXSS
});
$(event.target).on('shown.bs.popover', function() {
const popoverId = $(this).attr('aria-describedby');
$(`#${popoverId} .mdi-close`).click(() => {
$(this).popover('dispose');
});
});
$(event.target).popover('show');
const response = await fetch(saveRequestTaskInfoUrl);
const saveRequestTaskInfo = await response.json();
let content;
if ($.isEmptyObject(saveRequestTaskInfo)) {
content = 'Not available';
} else if (saveRequestTaskInfo.note != null) {
content = saveRequestTaskInfo.note;
} else {
const saveRequestInfo = [];
const taskData = {
'Type': ['raw', 'type'],
'Visit status': ['raw', 'visit_status'],
'Arguments': ['json', 'arguments'],
'Id': ['raw', 'id'],
'Backend id': ['raw', 'backend_id'],
'Scheduling date': ['date', 'scheduled'],
'Start date': ['date', 'started'],
'Completion date': ['date', 'ended'],
'Duration': ['duration', 'duration'],
'Runner': ['raw', 'worker'],
'Log': ['raw', 'message']
};
for (const [title, [type, property]] of Object.entries(taskData)) {
if (saveRequestTaskInfo.hasOwnProperty(property)) {
saveRequestInfo.push({
key: title,
value: formatValuePerType(type, saveRequestTaskInfo[property])
});
}
}
content = '';
for (const info of saveRequestInfo) {
content +=
`
${info.key}
${info.value}
`;
}
content += '
';
}
$('.swh-popover').html(content);
$(event.target).popover('update');
}
export function fillSaveRequestFormAndScroll(visitType, originUrl) {
$('#swh-input-origin-url').val(originUrl);
let originTypeFound = false;
$('#swh-input-visit-type option').each(function() {
const val = $(this).val();
if (val && originUrl.includes(val)) {
$(this).prop('selected', true);
originTypeFound = true;
}
});
if (!originTypeFound) {
$('#swh-input-visit-type option').each(function() {
const val = $(this).val();
if (val === visitType) {
$(this).prop('selected', true);
}
});
}
window.scrollTo(0, 0);
}
diff --git a/cypress/integration/origin-save.spec.js b/cypress/integration/origin-save.spec.js
index 6513cedd..e502687f 100644
--- a/cypress/integration/origin-save.spec.js
+++ b/cypress/integration/origin-save.spec.js
@@ -1,776 +1,778 @@
/**
* Copyright (C) 2019-2021 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
let url;
let origin;
const $ = Cypress.$;
const saveCodeMsg = {
'success': 'The "save code now" request has been accepted and will be processed as soon as possible.',
'warning': 'The "save code now" request has been put in pending state and may be accepted for processing after manual review.',
'rejected': 'The "save code now" request has been rejected because the provided origin url is blacklisted.',
'rateLimit': 'The rate limit for "save code now" requests has been reached. Please try again later.',
'not-found': 'The provided url does not exist',
'unknownError': 'An unexpected error happened when submitting the "save code now request',
'csrfError': 'CSRF Failed: Referrer checking failed - no Referrer.'
};
const anonymousVisitTypes = ['bzr', 'cvs', 'git', 'hg', 'svn'];
const allVisitTypes = ['archives', 'bzr', 'cvs', 'git', 'hg', 'svn'];
function makeOriginSaveRequest(originType, originUrl) {
cy.get('#swh-input-origin-url')
.type(originUrl)
.get('#swh-input-visit-type')
.select(originType)
.get('#swh-save-origin-form')
.submit();
}
function checkAlertVisible(alertType, msg) {
cy.get('#swh-origin-save-request-status')
.should('be.visible')
.find(`.alert-${alertType}`)
.should('be.visible')
.and('contain', msg);
}
// Stub requests to save an origin
function stubSaveRequest({
requestUrl,
visitType = 'git',
saveRequestStatus,
originUrl,
saveTaskStatus,
responseStatus = 200,
// For error code with the error message in the 'reason' key response
errorMessage = '',
saveRequestDate = new Date(),
visitDate = new Date(),
visitStatus = null
} = {}) {
let response;
if (responseStatus !== 200 && errorMessage) {
response = {
'reason': errorMessage
};
} else {
response = genOriginSaveResponse({visitType: visitType,
saveRequestStatus: saveRequestStatus,
originUrl: originUrl,
saveRequestDate: saveRequestDate,
saveTaskStatus: saveTaskStatus,
visitDate: visitDate,
visitStatus: visitStatus
});
}
cy.intercept('POST', requestUrl, {body: response, statusCode: responseStatus})
.as('saveRequest');
}
// Mocks API response : /save/(:visit_type)/(:origin_url)
// visit_type : {'git', 'hg', 'svn', ...}
function genOriginSaveResponse({
visitType = 'git',
saveRequestStatus,
originUrl,
saveRequestDate = new Date(),
saveTaskStatus,
visitDate = new Date(),
visitStatus
} = {}) {
return {
'visit_type': visitType,
'save_request_status': saveRequestStatus,
'origin_url': originUrl,
'id': 1,
'save_request_date': saveRequestDate ? saveRequestDate.toISOString() : null,
'save_task_status': saveTaskStatus,
'visit_date': visitDate ? visitDate.toISOString() : null,
'visit_status': visitStatus
};
};
describe('Origin Save Tests', function() {
before(function() {
url = this.Urls.origin_save();
origin = this.origin[0];
this.originSaveUrl = this.Urls.api_1_save_origin(origin.type, origin.url);
});
beforeEach(function() {
cy.fixture('origin-save').as('originSaveJSON');
cy.fixture('save-task-info').as('saveTaskInfoJSON');
cy.visit(url);
});
it('should format appropriately values depending on their type', function() {
const inputValues = [ // null values stay null
{type: 'json', value: null, expectedValue: null},
{type: 'date', value: null, expectedValue: null},
{type: 'raw', value: null, expectedValue: null},
{type: 'duration', value: null, expectedValue: null},
// non null values formatted depending on their type
{type: 'json', value: '{}', expectedValue: '"{}"'},
{type: 'date', value: '04/04/2021 01:00:00', expectedValue: '4/4/2021, 1:00:00 AM'},
{type: 'raw', value: 'value-for-identity', expectedValue: 'value-for-identity'},
{type: 'duration', value: '10', expectedValue: '10 seconds'},
{type: 'duration', value: 100, expectedValue: '100 seconds'}
];
cy.window().then(win => {
inputValues.forEach(function(input, index, array) {
const actualValue = win.swh.save.formatValuePerType(input.type, input.value);
assert.equal(actualValue, input.expectedValue);
});
});
});
it('should display accepted message when accepted', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'accepted',
originUrl: origin.url,
saveTaskStatus: 'not yet scheduled'});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should validate gitlab subproject url', function() {
const gitlabSubProjectUrl = 'https://gitlab.com/user/project/sub/';
const originSaveUrl = this.Urls.api_1_save_origin('git', gitlabSubProjectUrl);
stubSaveRequest({requestUrl: originSaveUrl,
saveRequestStatus: 'accepted',
originurl: gitlabSubProjectUrl,
saveTaskStatus: 'not yet scheduled'});
makeOriginSaveRequest('git', gitlabSubProjectUrl);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should validate project url with _ in username', function() {
const gitlabSubProjectUrl = 'https://gitlab.com/user_name/project.git';
const originSaveUrl = this.Urls.api_1_save_origin('git', gitlabSubProjectUrl);
stubSaveRequest({requestUrl: originSaveUrl,
saveRequestStatus: 'accepted',
originurl: gitlabSubProjectUrl,
saveTaskStatus: 'not yet scheduled'});
makeOriginSaveRequest('git', gitlabSubProjectUrl);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should validate git repo url starting with https://git.code.sf.net/u/', function() {
const sfUserGirProjectUrl = 'https://git.code.sf.net/u/username/project.git';
const originSaveUrl = this.Urls.api_1_save_origin('git', sfUserGirProjectUrl);
stubSaveRequest({requestUrl: originSaveUrl,
saveRequestStatus: 'accepted',
originurl: sfUserGirProjectUrl,
saveTaskStatus: 'not yet scheduled'});
makeOriginSaveRequest('git', sfUserGirProjectUrl);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should display warning message when pending', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'pending',
originUrl: origin.url,
saveTaskStatus: 'not created'});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('warning', saveCodeMsg['warning']);
});
});
it('should show error when the origin does not exist (status: 400)', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
originUrl: origin.url,
responseStatus: 400,
errorMessage: saveCodeMsg['not-found']});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('danger', saveCodeMsg['not-found']);
});
});
it('should show error when csrf validation failed (status: 403)', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'rejected',
originUrl: origin.url,
saveTaskStatus: 'not created',
responseStatus: 403,
errorMessage: saveCodeMsg['csrfError']});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('danger', saveCodeMsg['csrfError']);
});
});
it('should show error when origin is rejected (status: 403)', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'rejected',
originUrl: origin.url,
saveTaskStatus: 'not created',
responseStatus: 403,
errorMessage: saveCodeMsg['rejected']});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('danger', saveCodeMsg['rejected']);
});
});
it('should show error when rate limited (status: 429)', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'Request was throttled. Expected available in 60 seconds.',
originUrl: origin.url,
saveTaskStatus: 'not created',
responseStatus: 429});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('danger', saveCodeMsg['rateLimit']);
});
});
it('should show error when unknown error occurs (status other than 200, 403, 429)', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'Error',
originUrl: origin.url,
saveTaskStatus: 'not created',
responseStatus: 406});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('danger', saveCodeMsg['unknownError']);
});
});
it('should display origin save info in the requests table', function() {
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('tbody tr').then(rows => {
let i = 0;
for (const row of rows) {
const cells = row.cells;
const requestDateStr = new Date(this.originSaveJSON.data[i].save_request_date).toLocaleString();
const saveStatus = this.originSaveJSON.data[i].save_task_status;
assert.equal($(cells[0]).text(), requestDateStr);
assert.equal($(cells[1]).text(), this.originSaveJSON.data[i].visit_type);
let html = '';
if (saveStatus === 'succeeded') {
let browseOriginUrl = `${this.Urls.browse_origin()}?origin_url=${encodeURIComponent(this.originSaveJSON.data[i].origin_url)}`;
browseOriginUrl += `×tamp=${encodeURIComponent(this.originSaveJSON.data[i].visit_date)}`;
html += `${this.originSaveJSON.data[i].origin_url} `;
} else {
html += this.originSaveJSON.data[i].origin_url;
}
html += ` `;
html += ' ';
assert.equal($(cells[2]).html(), html);
assert.equal($(cells[3]).text(), this.originSaveJSON.data[i].save_request_status);
assert.equal($(cells[4]).text(), saveStatus);
++i;
}
});
});
it('should not add timestamp to the browse origin URL is no visit date has been found', function() {
const originUrl = 'https://git.example.org/example.git';
const saveRequestData = genOriginSaveResponse({
saveRequestStatus: 'accepted',
originUrl: originUrl,
saveTaskStatus: 'succeeded',
visitDate: null,
visitStatus: 'full'
});
const saveRequestsListData = {
'recordsTotal': 1,
'draw': 2,
'recordsFiltered': 1,
'data': [saveRequestData]
};
cy.intercept('/save/requests/list/**', {body: saveRequestsListData})
.as('saveRequestsList');
cy.get('#swh-origin-save-requests-list-tab').click();
cy.wait('@saveRequestsList');
cy.get('tbody tr').then(rows => {
const firstRowCells = rows[0].cells;
const browseOriginUrl = `${this.Urls.browse_origin()}?origin_url=${encodeURIComponent(originUrl)}`;
const browseOriginLink = `${originUrl} `;
expect($(firstRowCells[2]).html()).to.have.string(browseOriginLink);
});
});
it('should not add link to browse an origin when there is no visit status', function() {
const originUrl = 'https://git.example.org/example.git';
const saveRequestData = genOriginSaveResponse({
saveRequestStatus: 'accepted',
originUrl: originUrl,
saveTaskStatus: 'succeeded',
visitDate: null,
visitStatus: null
});
const saveRequestsListData = {
'recordsTotal': 1,
'draw': 2,
'recordsFiltered': 1,
'data': [saveRequestData]
};
cy.intercept('/save/requests/list/**', {body: saveRequestsListData})
.as('saveRequestsList');
cy.get('#swh-origin-save-requests-list-tab').click();
cy.wait('@saveRequestsList');
cy.get('tbody tr').then(rows => {
const firstRowCells = rows[0].cells;
const tooltip = 'origin was successfully loaded, waiting for data to be available in database';
const expectedContent = `${originUrl} `;
expect($(firstRowCells[2]).html()).to.have.string(expectedContent);
});
});
it('should display/close task info popover when clicking on the info button', function() {
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
cy.intercept('/save/task/info/**', {fixture: 'save-task-info'});
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('.swh-save-request-info')
.eq(0)
.click();
cy.get('.swh-save-request-info-popover')
.should('be.visible');
cy.get('.swh-save-request-info')
.eq(0)
.click();
cy.get('.swh-save-request-info-popover')
.should('not.exist');
});
it('should hide task info popover when clicking on the close button', function() {
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
cy.intercept('/save/task/info/**', {fixture: 'save-task-info'});
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('.swh-save-request-info')
.eq(0)
.click();
cy.get('.swh-save-request-info-popover')
.should('be.visible');
cy.get('.swh-save-request-info-close')
.click();
cy.get('.swh-save-request-info-popover')
.should('not.exist');
});
it('should fill save request form when clicking on "Save again" button', function() {
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('.swh-save-origin-again')
.eq(0)
.click();
cy.get('tbody tr').eq(0).then(row => {
const cells = row[0].cells;
cy.get('#swh-input-visit-type')
.should('have.value', $(cells[1]).text());
cy.get('#swh-input-origin-url')
.should('have.value', $(cells[2]).text().slice(0, -1));
});
});
it('should select correct visit type if possible when clicking on "Save again" button', function() {
const originUrl = 'https://gitlab.inria.fr/solverstack/maphys/maphys/';
const badVisitType = 'hg';
const goodVisitType = 'git';
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
stubSaveRequest({requestUrl: this.Urls.api_1_save_origin(badVisitType, originUrl),
visitType: badVisitType,
saveRequestStatus: 'accepted',
originUrl: originUrl,
saveTaskStatus: 'failed',
visitStatus: 'failed',
responseStatus: 200,
errorMessage: saveCodeMsg['accepted']});
makeOriginSaveRequest(badVisitType, originUrl);
cy.get('#swh-origin-save-requests-list-tab').click();
cy.wait('@saveRequest').then(() => {
cy.get('.swh-save-origin-again')
.eq(0)
.click();
cy.get('tbody tr').eq(0).then(row => {
const cells = row[0].cells;
cy.get('#swh-input-visit-type')
.should('have.value', goodVisitType);
cy.get('#swh-input-origin-url')
.should('have.value', $(cells[2]).text().slice(0, -1));
});
});
});
it('should create save request for authenticated user', function() {
cy.userLogin();
cy.visit(url);
const originUrl = 'https://git.example.org/account/repo';
stubSaveRequest({requestUrl: this.Urls.api_1_save_origin('git', originUrl),
saveRequestStatus: 'accepted',
originUrl: origin.url,
saveTaskStatus: 'not yet scheduled'});
makeOriginSaveRequest('git', originUrl);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should not show user requests filter checkbox for anonymous users', function() {
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('#swh-save-requests-user-filter').should('not.exist');
});
it('should show user requests filter checkbox for authenticated users', function() {
cy.userLogin();
cy.visit(url);
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('#swh-save-requests-user-filter').should('exist');
});
it('should show only user requests when filter is activated', function() {
cy.intercept('POST', '/api/1/origin/save/**')
.as('saveRequest');
const originAnonymousUser = 'https://some.git.server/project/';
const originAuthUser = 'https://other.git.server/project/';
// anonymous user creates a save request
makeOriginSaveRequest('git', originAnonymousUser);
cy.wait('@saveRequest');
// authenticated user creates another save request
cy.userLogin();
cy.visit(url);
makeOriginSaveRequest('git', originAuthUser);
cy.wait('@saveRequest');
// user requests filter checkbox should be in the DOM
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('#swh-save-requests-user-filter').should('exist');
// check unfiltered user requests
cy.get('tbody tr').then(rows => {
expect(rows.length).to.eq(2);
expect($(rows[0].cells[2]).text()).to.contain(originAuthUser);
expect($(rows[1].cells[2]).text()).to.contain(originAnonymousUser);
});
// activate filter and check filtered user requests
cy.get('#swh-save-requests-user-filter')
.click({force: true});
cy.get('tbody tr').then(rows => {
expect(rows.length).to.eq(1);
expect($(rows[0].cells[2]).text()).to.contain(originAuthUser);
});
// deactivate filter and check unfiltered user requests
cy.get('#swh-save-requests-user-filter')
.click({force: true});
cy.get('tbody tr').then(rows => {
expect(rows.length).to.eq(2);
});
});
it('should list unprivileged visit types when not connected', function() {
cy.visit(url);
cy.get('#swh-input-visit-type').children('option').then(options => {
const actual = [...options].map(o => o.value);
expect(actual).to.deep.eq(anonymousVisitTypes);
});
});
it('should list unprivileged visit types when connected as unprivileged user', function() {
cy.userLogin();
cy.visit(url);
cy.get('#swh-input-visit-type').children('option').then(options => {
const actual = [...options].map(o => o.value);
expect(actual).to.deep.eq(anonymousVisitTypes);
});
});
it('should list privileged visit types when connected as ambassador', function() {
cy.ambassadorLogin();
cy.visit(url);
cy.get('#swh-input-visit-type').children('option').then(options => {
const actual = [...options].map(o => o.value);
expect(actual).to.deep.eq(allVisitTypes);
});
});
it('should display extra inputs when dealing with \'archives\' visit type', function() {
cy.ambassadorLogin();
cy.visit(url);
for (const visitType of anonymousVisitTypes) {
cy.get('#swh-input-visit-type').select(visitType);
cy.get('.swh-save-origin-archives-form').should('not.be.visible');
}
// this should display more inputs with the 'archives' type
cy.get('#swh-input-visit-type').select('archives');
cy.get('.swh-save-origin-archives-form').should('be.visible');
});
it('should be allowed to submit \'archives\' save request when connected as ambassador', function() {
const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
const artifactVersion = '1.1.4';
stubSaveRequest({
requestUrl: this.Urls.api_1_save_origin('archives', originUrl),
saveRequestStatus: 'accepted',
originUrl: originUrl,
saveTaskStatus: 'not yet scheduled'
});
cy.ambassadorLogin();
cy.visit(url);
// input new 'archives' information and submit
cy.get('#swh-input-origin-url')
.type(originUrl)
.get('#swh-input-visit-type')
.select('archives')
.get('#swh-input-artifact-url-0')
.type(artifactUrl)
.get('#swh-input-artifact-version-0')
.clear()
.type(artifactVersion)
.get('#swh-save-origin-form')
.submit();
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should submit multiple artifacts for the archives visit type', function() {
const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
const artifactVersion = '1.1.4';
const artifact2Url = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.5.tar.gz';
const artifact2Version = '1.1.5';
cy.ambassadorLogin();
cy.visit(url);
cy.get('#swh-input-origin-url')
.type(originUrl)
.get('#swh-input-visit-type')
.select('archives');
// fill first artifact info
cy.get('#swh-input-artifact-url-0')
.type(artifactUrl)
.get('#swh-input-artifact-version-0')
.clear()
.type(artifactVersion);
// add new artifact form row
cy.get('#swh-add-archive-artifact')
.click();
// check new row is displayed
cy.get('#swh-input-artifact-url-1')
.should('exist');
// request removal of newly added row
cy.get('#swh-remove-archive-artifact-1')
.click();
// check row has been removed
cy.get('#swh-input-artifact-url-1')
.should('not.exist');
// add new artifact form row
cy.get('#swh-add-archive-artifact')
.click();
// fill second artifact info
cy.get('#swh-input-artifact-url-1')
.type(artifact2Url)
.get('#swh-input-artifact-version-1')
.clear()
.type(artifact2Version);
// setup request interceptor to check POST data and stub response
cy.intercept('POST', this.Urls.api_1_save_origin('archives', originUrl), (req) => {
expect(req.body).to.deep.equal({
archives_data: [
{artifact_url: artifactUrl, artifact_version: artifactVersion},
{artifact_url: artifact2Url, artifact_version: artifact2Version}
]
});
req.reply(genOriginSaveResponse({
visitType: 'archives',
saveRequestStatus: 'accepted',
originUrl: originUrl,
saveRequestDate: new Date(),
saveTaskStatus: 'not yet scheduled',
visitDate: null,
visitStatus: null
}));
}).as('saveRequest');
// submit form
cy.get('#swh-save-origin-form')
.submit();
// submission should be successful
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should autofill artifact version when pasting artifact url', function() {
const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
const artifactVersion = '3DLDF-1.1.4';
const artifact2Url = 'https://example.org/artifact/test/1.3.0.zip';
const artifact2Version = '1.3.0';
cy.ambassadorLogin();
cy.visit(url);
cy.get('#swh-input-origin-url')
.type(originUrl)
.get('#swh-input-visit-type')
.select('archives');
// fill first artifact info
cy.get('#swh-input-artifact-url-0')
.type(artifactUrl);
// check autofilled version
cy.get('#swh-input-artifact-version-0')
.should('have.value', artifactVersion);
// add new artifact form row
cy.get('#swh-add-archive-artifact')
.click();
// fill second artifact info
cy.get('#swh-input-artifact-url-1')
.type(artifact2Url);
// check autofilled version
cy.get('#swh-input-artifact-version-1')
.should('have.value', artifact2Version);
});
it('should use canonical URL for github repository to save', function() {
const ownerRepo = 'BIC-MNI/mni_autoreg';
const canonicalOriginUrl = 'https://github.com/BIC-MNI/mni_autoreg';
// stub call to github Web API fetching canonical repo URL
cy.intercept(`https://api.github.com/repos/${ownerRepo.toLowerCase()}`, (req) => {
req.reply({html_url: canonicalOriginUrl});
}).as('ghWebApiRequest');
// stub save request creation with canonical URL of github repo
cy.intercept('POST', this.Urls.api_1_save_origin('git', canonicalOriginUrl), (req) => {
req.reply(genOriginSaveResponse({
visitType: 'git',
saveRequestStatus: 'accepted',
originUrl: canonicalOriginUrl,
saveRequestDate: new Date(),
saveTaskStatus: 'not yet scheduled',
visitDate: null,
visitStatus: null
}));
}).as('saveRequest');
for (const originUrl of ['https://github.com/BiC-MnI/MnI_AuToReG',
'https://github.com/BiC-MnI/MnI_AuToReG.git',
'https://github.com/BiC-MnI/MnI_AuToReG/',
'https://BiC-MnI.github.io/MnI_AuToReG/'
]) {
// enter non canonical URL of github repo
cy.get('#swh-input-origin-url')
.clear()
.type(originUrl);
// submit form
cy.get('#swh-save-origin-form')
.submit();
// submission should be successful
cy.wait('@ghWebApiRequest')
.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
}
});
it('should switch tabs when playing with browser history', function() {
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
cy.intercept('/save/task/info/**', {fixture: 'save-task-info'});
cy.get('#swh-origin-save-request-help-tab')
.should('have.class', 'active');
cy.get('#swh-origin-save-requests-list-tab')
- .click()
+ .click();
+
+ cy.get('#swh-origin-save-requests-list-tab')
.should('have.class', 'active');
cy.go('back')
.get('#swh-origin-save-request-help-tab')
.should('have.class', 'active');
cy.go('forward')
.get('#swh-origin-save-requests-list-tab')
.should('have.class', 'active');
});
});
diff --git a/swh/web/misc/origin_save.py b/swh/web/misc/origin_save.py
index e66f00d2..6ae16959 100644
--- a/swh/web/misc/origin_save.py
+++ b/swh/web/misc/origin_save.py
@@ -1,99 +1,113 @@
# Copyright (C) 2018-2021 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from django.conf.urls import url
from django.core.paginator import Paginator
from django.db.models import Q
from django.http import JsonResponse
from django.shortcuts import render
from swh.web.auth.utils import SWH_AMBASSADOR_PERMISSION, privileged_user
from swh.web.common.models import SaveOriginRequest
from swh.web.common.origin_save import (
get_savable_visit_types,
get_save_origin_task_info,
)
-def _origin_save_view(request):
+def _origin_save_help_view(request):
return render(
request,
- "misc/origin-save.html",
+ "misc/origin-save-help.html",
+ {
+ "heading": ("Request the saving of a software origin into the archive"),
+ "visit_types": get_savable_visit_types(
+ privileged_user(request, permissions=[SWH_AMBASSADOR_PERMISSION])
+ ),
+ },
+ )
+
+
+def _origin_save_list_view(request):
+ return render(
+ request,
+ "misc/origin-save-list.html",
{
"heading": ("Request the saving of a software origin into the archive"),
"visit_types": get_savable_visit_types(
privileged_user(request, permissions=[SWH_AMBASSADOR_PERMISSION])
),
},
)
def _origin_save_requests_list(request, status):
if status != "all":
save_requests = SaveOriginRequest.objects.filter(status=status)
else:
save_requests = SaveOriginRequest.objects.all()
table_data = {}
table_data["recordsTotal"] = save_requests.count()
table_data["draw"] = int(request.GET["draw"])
search_value = request.GET["search[value]"]
column_order = request.GET["order[0][column]"]
field_order = request.GET["columns[%s][name]" % column_order]
order_dir = request.GET["order[0][dir]"]
if order_dir == "desc":
field_order = "-" + field_order
save_requests = save_requests.order_by(field_order)
length = int(request.GET["length"])
page = int(request.GET["start"]) / length + 1
if search_value:
save_requests = save_requests.filter(
Q(status__icontains=search_value)
| Q(loading_task_status__icontains=search_value)
| Q(visit_type__icontains=search_value)
| Q(origin_url__icontains=search_value)
)
if (
int(request.GET.get("user_requests_only", "0"))
and request.user.is_authenticated
):
save_requests = save_requests.filter(user_ids__contains=f'"{request.user.id}"')
table_data["recordsFiltered"] = save_requests.count()
paginator = Paginator(save_requests, length)
table_data["data"] = [sor.to_dict() for sor in paginator.page(page).object_list]
return JsonResponse(table_data)
def _save_origin_task_info(request, save_request_id):
request_info = get_save_origin_task_info(
save_request_id, full_info=request.user.is_staff
)
for date_field in ("scheduled", "started", "ended"):
if date_field in request_info and request_info[date_field] is not None:
request_info[date_field] = request_info[date_field].isoformat()
return JsonResponse(request_info)
urlpatterns = [
- url(r"^save/$", _origin_save_view, name="origin-save"),
+ url(r"^save/$", _origin_save_help_view, name="origin-save"),
+ url(r"^save/list/$", _origin_save_list_view, name="origin-save-list"),
url(
r"^save/requests/list/(?P.+)/$",
_origin_save_requests_list,
name="origin-save-requests-list",
),
url(
r"^save/task/info/(?P.+)/$",
_save_origin_task_info,
name="origin-save-task-info",
),
]
diff --git a/swh/web/templates/misc/origin-save-help.html b/swh/web/templates/misc/origin-save-help.html
new file mode 100644
index 00000000..05c9fadb
--- /dev/null
+++ b/swh/web/templates/misc/origin-save-help.html
@@ -0,0 +1,54 @@
+{% extends "./origin-save.html" %}
+
+{% comment %}
+Copyright (C) 2018-2021 The Software Heritage developers
+See the AUTHORS file at the top-level directory of this distribution
+License: GNU Affero General Public License version 3, or any later version
+See top-level LICENSE file for more information
+{% endcomment %}
+
+{% block tab_content %}
+
+
A "Save code now" request takes the following parameters:
+
+ Origin type: the type of version control system the software origin is using.
+ Currently, the supported types are:
+
+ git
, for origins using Git
+ hg
, for origins using Mercurial
+ svn
, for origins using Subversion
+ {% if "cvs" in visit_types %}
+ cvs
, for origins using CVS
+ {% endif %}
+ {% if "bzr" in visit_types %}
+ bzr
, for origins using Bazaar
+ {% endif %}
+
+
+ Origin url: the url of the remote repository for the software origin.
+ In order to avoid saving errors from Software Heritage, you should provide the clone/checkout url
+ as given by the provider hosting the software origin. It can easily be found in the
+ web interface used to browse the software origin. For instance, if you want to save a git
+ origin into the archive, you should check that the command $ git clone <origin_url>
+ does not return an error before submitting a request.
+
+
+
+ Once submitted, your save request can either be:
+
+
+ accepted: a visit to the provided origin will then be scheduled by Software Heritage in order to
+ load its content into the archive as soon as possible
+ rejected: the provided origin url is blacklisted and no visit will be scheduled
+ put in pending state: a manual review will then be performed in order to determine if the
+ origin can be safely loaded or not into the archive
+
+
+ Once a save request has been accepted, you can follow its current status in the
+ submitted save requests list .
+
+ If you submitted requests while authenticated , you will be able
+ to only display your own requests.
+
+
+{% endblock %}
diff --git a/swh/web/templates/misc/origin-save-list.html b/swh/web/templates/misc/origin-save-list.html
new file mode 100644
index 00000000..c91540da
--- /dev/null
+++ b/swh/web/templates/misc/origin-save-list.html
@@ -0,0 +1,27 @@
+{% extends "./origin-save.html" %}
+
+{% comment %}
+Copyright (C) 2018-2021 The Software Heritage developers
+See the AUTHORS file at the top-level directory of this distribution
+License: GNU Affero General Public License version 3, or any later version
+See top-level LICENSE file for more information
+{% endcomment %}
+
+{% block tab_content %}
+
+
+
+
+ Date
+ Type
+ Url
+ Request
+ Status
+ Info
+
+
+
+
+
+
+{% endblock %}
diff --git a/swh/web/templates/misc/origin-save.html b/swh/web/templates/misc/origin-save.html
index fdc520a1..feecc0ec 100644
--- a/swh/web/templates/misc/origin-save.html
+++ b/swh/web/templates/misc/origin-save.html
@@ -1,147 +1,88 @@
{% extends "../layout.html" %}
{% comment %}
Copyright (C) 2018-2021 The Software Heritage developers
See the AUTHORS file at the top-level directory of this distribution
License: GNU Affero General Public License version 3, or any later version
See top-level LICENSE file for more information
{% endcomment %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% block title %}{{ heading }} – Software Heritage archive{% endblock %}
{% block header %}
{% render_bundle 'save' %}
{% endblock %}
{% block navbar-content %}
Save code now
{% endblock %}
{% block content %}
You can contribute to extend the content of the Software Heritage archive by submitting an origin
save request. To do so, fill the required info in the form below:
Processing "save code now" request ...
-
-
A "Save code now" request takes the following parameters:
-
- Origin type: the type of version control system the software origin is using.
- Currently, the supported types are:
-
- git
, for origins using Git
- hg
, for origins using Mercurial
- svn
, for origins using Subversion
- {% if "cvs" in visit_types %}
- cvs
, for origins using CVS
- {% endif %}
- {% if "bzr" in visit_types %}
- bzr
, for origins using Bazaar
- {% endif %}
-
-
- Origin url: the url of the remote repository for the software origin.
- In order to avoid saving errors from Software Heritage, you should provide the clone/checkout url
- as given by the provider hosting the software origin. It can easily be found in the
- web interface used to browse the software origin. For instance, if you want to save a git
- origin into the archive, you should check that the command $ git clone <origin_url>
- does not return an error before submitting a request.
-
-
-
- Once submitted, your save request can either be:
-
-
- accepted: a visit to the provided origin will then be scheduled by Software Heritage in order to
- load its content into the archive as soon as possible
- rejected: the provided origin url is blacklisted and no visit will be scheduled
- put in pending state: a manual review will then be performed in order to determine if the
- origin can be safely loaded or not into the archive
-
-
- Once a save request has been accepted, you can follow its current status in the
- submitted save requests list .
-
- If you submitted requests while authenticated , you will be able
- to only display your own requests.
-
-
-
-
-
-
-
-
- Date
- Type
- Url
- Request
- Status
- Info
-
-
-
-
-
-
+{% block tab_content %}
+{% endblock %}
{% endblock %}